/*
 * Zettelkasten - nach Luhmann
 * Copyright (C) 2001-2015 by Daniel Lüdecke (http://www.danielluedecke.de)
 * 
 * Homepage: http://zettelkasten.danielluedecke.de
 * 
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of 
 * the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see <http://www.gnu.org/licenses/>.
 * 
 * 
 * Dieses Programm ist freie Software. Sie können es unter den Bedingungen der GNU
 * General Public License, wie von der Free Software Foundation veröffentlicht, weitergeben
 * und/oder modifizieren, entweder gemäß Version 3 der Lizenz oder (wenn Sie möchten)
 * jeder späteren Version.
 * 
 * Die Veröffentlichung dieses Programms erfolgt in der Hoffnung, daß es Ihnen von Nutzen sein 
 * wird, aber OHNE IRGENDEINE GARANTIE, sogar ohne die implizite Garantie der MARKTREIFE oder 
 * der VERWENDBARKEIT FÜR EINEN BESTIMMTEN ZWECK. Details finden Sie in der 
 * GNU General Public License.
 * 
 * Sie sollten ein Exemplar der GNU General Public License zusammen mit diesem Programm 
 * erhalten haben. Falls nicht, siehe <http://www.gnu.org/licenses/>.
 */
package de.danielluedecke.zettelkasten.util;

import de.danielluedecke.zettelkasten.database.Daten;
import de.danielluedecke.zettelkasten.settings.Settings;

import java.awt.FileDialog;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.swing.DefaultListModel;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;

import org.jdom2.Document;
import org.jdom2.input.SAXBuilder;

/**
 *
 * @author Luedeke
 */
public class FileOperationsUtil {

	/**
	 * create a variable for a list model. this list model is used for the
	 * JList-component which displays the links of the current entry.
	 */
	private static DefaultListModel<String> linkListModel = new DefaultListModel<String>();

	public static DefaultListModel<String> getListModel() {
		return linkListModel;
	}

	private static String[] addedAttachments = null;

	public static String[] getAddedAttachments() {
		return addedAttachments;
	}

	/**
	 * Returns XML Document inside zipFile with filename.
	 * 
	 * @param zipFile
	 * @param filename filename of the XML file inside the zip file to return.
	 * @return XML Document
	 * @throws Exception IO, XML or Not Found.
	 */
	public static Document readXMLFileFromZipFile(File zipFile, String filename) throws Exception {
		try (ZipInputStream zip = new ZipInputStream(new FileInputStream(zipFile))) {
			ZipEntry entry;
			while ((entry = zip.getNextEntry()) != null) {
				// Keep moving through the entries to find filenameToLoad.
				if (entry.getName().equals(filename)) {
					SAXBuilder builder = new SAXBuilder();
					Document doc = builder.build(zip);
					return doc;
				}
			}
		}
		throw new Exception(String.format("Filename %s not found in zip file %s", filename, zipFile.getPath()));
	}

	/**
	 * This method creates a backup-filepath by adding the extension <b>.backup</b>
	 * to the filepath given in {@code sourcefile}. In case this filepath already
	 * exists, this method tries to use a new extension, until a non-existing
	 * filepath was created.<br>
	 * <br>
	 * Results might be:<br>
	 * <i>sourcefile.ext.backup</i><br>
	 * <i>sourcefile.ext.backup-1</i><br>
	 * <i>sourcefile.ext.backup-2</i><br>
	 * and so on...
	 *
	 * @param sourcefile the filepath of the source-file that should be backed up
	 * @return a filepath-value with a non-existing backup-extenstion.
	 */
	public static File getBackupFilePath(File sourcefile) {
		// find correct extension. we use this in case we already
		// have several backups...
		// in case the user already created a backup, we concatenate a trainling
		// backup-counter-number to avoid overwriting existing backup-files
		// we start with a "1"
		int backupcounter = 1;
		String backupext = ".backup";
		File checkbackup = new File(sourcefile.toString() + backupext);
		while (checkbackup.exists()) {
			backupcounter++;
			backupext = ".backup-" + String.valueOf(backupcounter);
			checkbackup = new File(sourcefile.toString() + backupext);
		}
		return checkbackup;
	}

	/**
	 * Creates a mac-aqua-like file dialog. Since the typical OS X dialog can not be
	 * created using the swing file chooser, we use the awt-FileDialog instead to
	 * get mac-like-look'n'feel.
	 *
	 * @param fc          a reference to a FileDialog that should be initiated.
	 * @param dlgmode     either {@code FileDialog.LOAD} or {@code FileDialog.SAVE}
	 * @param initdir     the initial directory which can be set when the dialog is
	 *                    shown
	 * @param initfile    the initial file which can be selected when the dialog is
	 *                    shown
	 * @param title       the dialog's title
	 * @param acceptedext the accepted file extensions that will be accepted, i.e.
	 *                    the files that are selectable
	 * @return a reference to the created <i>and initiated</i> FileDialog which was
	 *         passed as parameter {@code fc}.
	 */
	static FileDialog getMacFileDialog(FileDialog fc, int dlgmode, String initdir, String initfile, String title,
			final String[] acceptedext) {
		fc.setTitle(title);
		fc.setMode(dlgmode);
		fc.setDirectory(initdir);
		fc.setFile(initfile);
		fc.setFilenameFilter(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				if (acceptedext != null && acceptedext.length > 0) {
					boolean acc = false;
					for (String ext : acceptedext) {
						if (name.toLowerCase().endsWith(ext)) {
							acc = true;
						}
					}
					return acc;
				} else {
					return true;
				}
			}
		});
		return fc;
	}

	/**
	 *
	 * @param f
	 * @param ext
	 * @return
	 */
	public static String setFileExtension(File f, String ext) {
		if (null == f || null == ext || ext.isEmpty()) {
			return null;
		}
		if (!ext.startsWith(".")) {
			ext = "." + ext;
		}
		String s = f.getName();
		int i = s.lastIndexOf(".");
		return s.substring(0, i - 1).concat(ext);
	}

	/**
	 * This method creates and shows a file chooser, depending on the operating
	 * system. In case the os is Windows or Linux, the standard Swing-JFileChooser
	 * will be opened. In case the os is Mac OS X, the old awt-dialog is used, which
	 * looks more nativ.<br>
	 * <br>
	 * When the user chose a file, it will be returned, else {@code null} will be
	 * returned.
	 *
	 * @param parent       the parent-frame of the file chooser
	 * @param dlgmode<br>
	 *                     - in case of Mac OS X: either {@code FileDialog.LOAD} or
	 *                     {@code FileDialog.SAVE} - else:
	 *                     {@code JFileChooser.OPEN_DIALOG} or
	 *                     {@code JFileChooser.SAVE_DIALOG}
	 * @param filemode<br>
	 *                     - not important for Mac OS X. - else:
	 *                     {@code JFileChooser.FILES_ONLY} or the other
	 *                     file-selection-mode-values
	 * @param initdir      the initial directory which can be set when the dialog is
	 *                     shown
	 * @param initfile     the initial file which can be selected when the dialog is
	 *                     shown
	 * @param title        the dialog's title
	 * @param acceptedext  the accepted file extensions that will be accepted, i.e.
	 *                     the files that are selectable
	 * @param desc         the description of which file types the extensions are
	 * @param settings     a reference to the CSettings-class
	 * @return The chosen file, or {@code null} if dialog was cancelled
	 */
	public static File chooseFile(java.awt.Frame parent, int dlgmode, int filemode, String initdir, String initfile,
			String title, final String[] acceptedext, final String desc, Settings settings) {
		if (!settings.isMacStyle()) {
			File curdir = (null == initdir) ? null : new File(initdir);
			JFileChooser fc = createFileChooser(title, filemode, curdir, acceptedext, desc);
			int option = (JFileChooser.OPEN_DIALOG == dlgmode) ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
			if (JFileChooser.APPROVE_OPTION == option) {
				return fc.getSelectedFile();
			}
		} else {
			FileDialog fd = getMacFileDialog(new FileDialog(parent), dlgmode, initdir, initfile, title, acceptedext);
			fd.setVisible(true);
			String file = fd.getFile();
			if (file != null) {
				return new File(fd.getDirectory() + fd.getFile());
			}
		}
		return null;
	}

	/**
	 * This method creates and shows a file chooser, depending on the operating
	 * system. In case the os is Windows or Linux, the standard Swing-JFileChooser
	 * will be opened. In case the os is Mac OS X, the old awt-dialog is used, which
	 * looks more nativ.<br>
	 * <br>
	 * When the user chose a file, it will be returned, else {@code null} will be
	 * returned.
	 *
	 * @param parent       the parent-dialog of the file chooser
	 * @param dlgmode<br>
	 *                     - in case of Mac OS X: either {@code FileDialog.LOAD} or
	 *                     {@code FileDialog.SAVE} - else:
	 *                     {@code JFileChooser.OPEN_DIALOG} or
	 *                     {@code JFileChooser.SAVE_DIALOG}
	 * @param filemode<br>
	 *                     - not important for Mac OS X. - else:
	 *                     {@code JFileChooser.FILES_ONLY} or the other
	 *                     file-selection-mode-values
	 * @param initdir      the initial directory which can be set when the dialog is
	 *                     shown
	 * @param initfile     the initial file which can be selected when the dialog is
	 *                     shown
	 * @param title        the dialog's title
	 * @param acceptedext  the accepted file extensions that will be accepted, i.e.
	 *                     the files that are selectable
	 * @param desc         the description of which file types the extensions are
	 * @param settings     a reference to the CSettings-class
	 * @return The chosen file, or {@code null} if dialog was cancelled
	 */
	public static File chooseFile(java.awt.Dialog parent, int dlgmode, int filemode, String initdir, String initfile,
			String title, final String[] acceptedext, final String desc, Settings settings) {
		if (!settings.isMacStyle()) {
			File curdir = (null == initdir) ? null : new File(initdir);
			JFileChooser fc = createFileChooser(title, filemode, curdir, acceptedext, desc);
			int option = (JFileChooser.OPEN_DIALOG == dlgmode) ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
			if (JFileChooser.APPROVE_OPTION == option) {
				return fc.getSelectedFile();
			}
		} else {
			FileDialog fd = getMacFileDialog(new FileDialog(parent), dlgmode, initdir, initfile, title, acceptedext);
			fd.setVisible(true);
			String file = fd.getFile();
			if (file != null) {
				return new File(fd.getDirectory() + fd.getFile());
			}
		}
		return null;
	}

	/**
	 * This method returns the file extension of a given file which is passed as
	 * parameter.
	 *
	 * @param f the file which extension should be retrieved
	 * @return the extension of the given file, <b>without</b> leading period (e.g.
	 *         "jpg" is returned, not ".jpg")
	 */
	public static String getFileExtension(File f) {
		if (null == f) {
			return "";
		}
		String ext = null;
		String s = f.getName();
		int i = s.lastIndexOf(".");
		if (i > 0 && i < s.length() - 1) {
			ext = s.substring(i + 1).toLowerCase();
		}
		return ext;
	}

	/**
	 * This method returns the file extension of a given file passed as
	 * string-parameter.
	 *
	 * @param settingsObj
	 * @param dataObj
	 * @param f           the file which extension should be retrieved
	 * @return the extension of the given file, <b>without</b> leading period (e.g.
	 *         "jpg" is returned, not ".jpg"), in upper case letters.
	 */
	public static String getFileExtension(Settings settingsObj, Daten dataObj, String f) {
		String ext = "";
		if (isHyperlink(f)) {
			return "URL";
		}
		f = getLinkFile(settingsObj, dataObj, f).toString();
		if (!new File(f).exists()) {
			return "";
		}
		int i = f.lastIndexOf(".");
		if (i > 0 && i < f.length() - 1) {
			ext = f.substring(i + 1).toUpperCase();
		}
		return ext;
	}

	/**
	 * This method returns the file name of a given file-path which is passed as
	 * parameter.
	 *
	 * @param f the filepath of the file which file name should be retrieved
	 * @return the name of the given file, excluding extension, or {@code null} if
	 *         an error occured.
	 */
	public static String getFileName(String f) {
		String fn = null;
		int i = f.lastIndexOf(String.valueOf(File.separatorChar));
		if (i != -1) {
			int j = f.lastIndexOf(".");
			if (j != -1) {
				try {
					fn = f.substring(i + 1, j);
				} catch (IndexOutOfBoundsException ex) {
					return null;
				}
			}
		}
		return fn;
	}

	/**
	 * This method returns the file path only of a given file which is passed as
	 * parameter, <em>excluding the file name</em>.
	 *
	 * @param f the filepath of the file which should be cleaned from file name
	 * @return the path of the given file, excluding file name, or an empty string
	 *         if an error occured.
	 */
	public static String getFilePath(String f) {
		String fn = "";
		int i = f.lastIndexOf(String.valueOf(File.separatorChar));
		if (i != -1) {
			try {
				fn = f.substring(0, i);
			} catch (IndexOutOfBoundsException ex) {
				return "";
			}
		}
		return fn;
	}

	/**
	 * This method returns the file path only of a given file which is passed as
	 * parameter, <em>excluding the file name</em>.
	 *
	 * @param f the filepath of the file which should be cleaned from file name
	 * @return the path of the given file, excluding file name, or an empty string
	 *         if an error occured.
	 */
	public static String getFilePath(File f) {
		try {
			return getFilePath(f.getCanonicalPath());
		} catch (IOException ex) {
			return "";
		}
	}

	/**
	 * This method returns the file name of a given file-path which is passed as
	 * parameter.
	 *
	 * @param f the filepath of the file which file name should be retrieved
	 * @return the name of the given file, excluding extension, or {@code null} if
	 *         an error occured.
	 */
	public static String getFileName(File f) {
		if (f != null) {
			String fname = f.getName();
			int extpos = fname.lastIndexOf(".");
			if (extpos != -1) {
				try {
					return fname.substring(0, extpos);
				} catch (IndexOutOfBoundsException ex) {
					return null;
				}
			}
		}
		return null;
	}

	/**
	 * This method copies a file. check for existing files is not done here, needs
	 * to be checked from within the method which calls this method.
	 *
	 * @param src     the source file that should be copied
	 * @param dest    the destination filename and path
	 * @param bufSize the buffer size
	 * @throws IOException
	 */
	public static void copyFile(File src, File dest, int bufSize) throws IOException {
		byte[] buffer = new byte[bufSize];
		InputStream in = null;
		OutputStream out = null;
		try {
			in = new FileInputStream(src);
			out = new FileOutputStream(dest);
			while (true) {
				int read = in.read(buffer);
				if (read == -1) {
					break;
				}
				out.write(buffer, 0, read);
			}
		} catch (IOException | NullPointerException e) {
		} finally {
			try {
				if (in != null) {
					in.close();
				}
				if (out != null) {
					out.close();
				}
			} catch (IOException e) {
			}
		}
	}

	/**
	 * createFileCopyIfExists copies sourceFile to newFile. It does nothing if
	 * sourceFile doesn't exist. It returns true if the sourceFile doesn't exist or
	 * the copy was a success; false otherwise.
	 * 
	 */
	public static boolean createFileCopyIfExists(File sourceFile, File newFile) {
		try {
			if (!sourceFile.exists()) {
				// Do nothing if source file does not exist.
				Constants.zknlogger.log(Level.WARNING, "Couldn't make a backup copy of {0} as it doesn't exist",
						sourceFile.toString());
				return true;
			}

			// Delete existing temporary file.
			if (newFile.exists()) {
				newFile.delete();
			}
			// Make copy of existing Zettelkasten Data file.
			FileOperationsUtil.copyFile(sourceFile, newFile, 1024);
		} catch (Exception e) {
			Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage());
			return false;
		}
		return true;
	}

	/**
	 * This merthod creates a FileChooser Dialog, initiates it (based on the passed
	 * parameters) and returns a reference to the FileChooser Dialog.
	 *
	 * @param name        the file dialog's title
	 * @param filemode    {@code JFileChooser.FILES_ONLY} or the other
	 *                    file-selection-mode-values (not important for Mac OS X)
	 * @param curdir      the initial directory which can be set when the dialog is
	 *                    shown
	 * @param acceptedext the accepted file extensions that will be accepted, i.e.
	 *                    the files that are selectable
	 * @param desc        the description of which file types the extensions are
	 * @return a reference to a new created file chooser
	 */
	public static JFileChooser createFileChooser(String name, int filemode, File curdir, final String[] acceptedext,
			final String desc) {
		JFileChooser fc = new JFileChooser();
		fc.setDialogTitle(name);
		fc.setAcceptAllFileFilterUsed(false);
		fc.setCurrentDirectory(curdir);
		fc.setFileSelectionMode(filemode);
		if (null == acceptedext || acceptedext.length < 1) {
			fc.setAcceptAllFileFilterUsed(true);
		} else {
			fc.setFileFilter(new FileFilter() {
				@Override
				public boolean accept(File f) {
					if (f.isDirectory()) {
						return true;
					}
					// set file extensions in the file dialog according
					// to user's choice of import type
					// other accepted files are located in the resource map
					boolean retval = false;
					String fileext = "." + getFileExtension(f);
					if (acceptedext != null && acceptedext.length > 0) {
						for (String ext : acceptedext) {
							if (fileext.equals(ext.toLowerCase())) {
								retval = true;
							}
						}
					}
					return retval;
				}

				@Override
				public String getDescription() {
					return desc;
				}
			});
		}
		return fc;
	}

	/**
	 * A directory-chooser for choosing directories (when file-selecting should be
	 * restricted).
	 *
	 * @param name   the dialog's title
	 * @param desc   the locale for "folders"
	 * @param curdir the initial directory to be set
	 * @return the selected directory as File-variable, or {@code null} if user
	 *         cancels the dialog
	 */
	public static File chooseDirectory(String name, final String desc, File curdir) {
		JFileChooser fc = new JFileChooser();
		fc.setDialogTitle(name);
		fc.setAcceptAllFileFilterUsed(false);
		fc.setCurrentDirectory(curdir);
		fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		fc.setFileFilter(new FileFilter() {
			@Override
			public boolean accept(File f) {
				return f.isDirectory();
			}

			@Override
			public String getDescription() {
				return desc;
			}
		});
		int option = fc.showOpenDialog(null);
		if (JFileChooser.APPROVE_OPTION == option) {
			return fc.getSelectedFile();
		}
		return null;
	}

	public static boolean insertAttachments(Daten dataObj, Settings settingsObj, JFrame parentFrame, File[] sources,
			DefaultListModel<String> lm) {
		org.jdesktop.application.ResourceMap resourceMap = org.jdesktop.application.Application
				.getInstance(de.danielluedecke.zettelkasten.ZettelkastenApp.class).getContext()
				.getResourceMap(FileOperationsUtil.class);
		if (lm != null) {
			linkListModel = lm;
		} else {
			linkListModel = new DefaultListModel<String>();
		}
		List<String> addedValues = new ArrayList<>();
		// modified tag
		boolean modified = false;
		// declare constants for moving/copying files
		// files should be copied
		final int ATT_COPY = 0;
		// files should be moved
		final int ATT_MOVE = 1;
		// files should remain in their original folder
		final int ATT_REMAIN = 2;
		// action should be cancelled
		final int ATT_CANCEL = 3;
		// declare constants for renaming/keeping files
		// files should be renamed
		final int ATT_RENAME = 0;
		// existing attachment file should be used
		final int ATT_USE_EXISTING = 1;
		// action should be cancelled
		final int ATT_CANCEL_RENAME = 2;
		// check whether we already have a saved data file or not. if not, we have no
		// related
		// path for the subdirectory "img", thus we cannot copy the images
		if (null == settingsObj.getMainDataFile() || !settingsObj.getMainDataFile().exists()) {
			// display error message box
			JOptionPane.showMessageDialog(parentFrame, resourceMap.getString("noDataFileSavedMsg"),
					resourceMap.getString("noDataFileSavedTitle"), JOptionPane.PLAIN_MESSAGE);
			return false;
		}
		// retrieve the application's directory and add an "/img/" as subfolder for
		// images
		// and append the file name of the image file. we need this string already now
		// for the
		// message box to tell the user where the file will be copied to.
		String destdir = settingsObj.getAttachmentPath(dataObj.getUserAttachmentPath(), true);
		// create new linked list that will contain a "cleaned" list of files, i.e. only
		// contains
		// those selected files that haven't been copied to the attachment directory
		// yet.
		LinkedList<File> newfiles = new LinkedList<>();
		// iterate array
		for (File cf : sources) {
			// first off all, let's check whether the user chose an already existing
			// attachment-file
			// which already has been copied to the application's attachment directory. if
			// so, no
			// new copy operation is needed, thus we *exclude* that file from the copy-list,
			// but already *include* it to the jList with attachments...
			// if the source file does not start with the same string part as the
			// application's
			// applications directory, we assume that the attachment has not been copied to
			// that directory yet.
			if (!cf.toString().startsWith(destdir)) {
				newfiles.add(cf);
			} else {
				// remove first part of the file-path, so we have the related path
				// to the attachment left
				String remainingpath = cf.toString().substring(destdir.length());
				// add attachment-value to list
				linkListModel.addElement(remainingpath);
				addedValues.add(remainingpath);
				// set the modified state
				modified = true;
			}
		}
		// if we don't have any new files that haven't been copied to the
		// attachment-directory before,
		// we can leave the method now...
		if (newfiles.size() < 1) {
			return modified;
		}
		// else create new array with files to be copied.
		sources = newfiles.toArray(new File[newfiles.size()]);
		// create a JOptionPane with moce/copy/cancel options
		int msgOption = JOptionPane.showOptionDialog(parentFrame,
				resourceMap.getString("msgConfirmAttachmentCopyMsg", destdir),
				resourceMap.getString("msgConfirmAttachmentCopyTitle"), JOptionPane.YES_NO_CANCEL_OPTION,
				JOptionPane.PLAIN_MESSAGE, null,
				new Object[] { resourceMap.getString("optionFileCopy"), resourceMap.getString("optionFileMove"),
						resourceMap.getString("optionFileRemain"), resourceMap.getString("optionFileCancel"), },
				resourceMap.getString("optioneFileCopy"));
		// if the user wants to proceed, copy the image now
		if (ATT_COPY == msgOption || ATT_MOVE == msgOption) {
			// first, check whether we already have an attachment directory
			// create the file-object with the necessary directory path
			File attachmentdir = new File(destdir);
			// if the directory does not exist, create it
			if (!attachmentdir.exists()) {
				// create directory
				try {
					if (!attachmentdir.mkdir()) {
						// if it fails, show warning message and leave method
						// create a message string including the filepath of the directory
						// which could not be created
						JOptionPane.showMessageDialog(parentFrame,
								resourceMap.getString("errMsgCreateAttDirMsg", attachmentdir),
								resourceMap.getString("errMsgCreateDirTitle"), JOptionPane.PLAIN_MESSAGE);
						return false;
					}
				} catch (SecurityException e) {
					Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage());
					// if it fails, show warning message and leave method
					// create a message string including the filepath of the directory
					// which could not be created
					JOptionPane.showMessageDialog(parentFrame,
							resourceMap.getString("errMsgCreateAttDirMsg", attachmentdir),
							resourceMap.getString("errMsgCreateDirTitle"), JOptionPane.PLAIN_MESSAGE);
					return false;
				}
			}
			// go through all selected files
			for (File f : sources) {
				// store the fileextension for later use, see below
				String fileextension = FileOperationsUtil.getFileExtension(f);
				// create a string to replace german umlauts
				// we have to do this because it seems like the Java desktop-api
				// is buggy. Files with umlauts in their names or paths cannot be
				// opend by the desktop-api, although their path is correct
				String withoutumlauts = f.getName();
				// replace umlauts with normal alphabetical letters
				withoutumlauts = withoutumlauts.replace("ä", "ae").replace("Ä", "Ae").replace("ö", "oe")
						.replace("Ö", "Oe").replace("ü", "ue").replace("Ü", "Ue").replace("ß", "ss").replace("\"", "");
				// create destionation file
				File dest = new File(destdir + withoutumlauts);
				// create loop-indicator
				boolean dest_ok = false;
				// indicator whether further action is needed
				boolean copyneeded = true;
				// check whether the file exists. if yes, the user should enter another name
				while (!dest_ok) {
					// check whether file exists
					if (dest.exists()) {
						// tell user that file exist and ask whether file should be renamed, or
						// existing file should be used as value
						// create a JOptionPane with rename/use exiting file options
						int msgOpt = JOptionPane.showOptionDialog(parentFrame,
								resourceMap.getString("msgFileExistsChoice"),
								resourceMap.getString("msgFileExistsChoiceTitle"), JOptionPane.YES_NO_CANCEL_OPTION,
								JOptionPane.PLAIN_MESSAGE, null,
								new Object[] { resourceMap.getString("optionFileRename"),
										resourceMap.getString("optionFileUseExisting"),
										resourceMap.getString("optionFileCancel"), },
								resourceMap.getString("optionFileUseExisting"));
						// if action cancelled, quit
						if (ATT_CANCEL_RENAME == msgOpt) {
							return false;
						}
						// if existing attachment should be used, do this here
						if (ATT_USE_EXISTING == msgOpt) {
							// add copied/moves file to link list...
							linkListModel.addElement((String) dest.getName());
							addedValues.add((String) dest.getName());
							// set the modified state
							modified = true;
							// indicate that while-loop is over, destination is valid
							dest_ok = true;
							// no more copying needed
							copyneeded = false;
						} else if (ATT_RENAME == msgOpt) {
							// open an option dialog and let the user prompt a new filename
							Object fnobject = JOptionPane.showInputDialog(parentFrame,
									resourceMap.getString("msgFileExists"), resourceMap.getString("msgFileExistsTitle"),
									JOptionPane.PLAIN_MESSAGE, null, null, dest.getName());
							// if the user cancelled the dialog, quit method
							if (null == fnobject) {
								return false;
							}
							// else copy object to string
							String newfilename = fnobject.toString();
							// check whether the user just typed in a name without extension
							// if so, add extension here
							if (!newfilename.endsWith("." + fileextension)) {
								newfilename = newfilename.concat("." + fileextension);
							}
							// and create a new file
							dest = new File(destdir + newfilename);
						}
					} else {
						// indicate that while-loop is over, destination is valid
						dest_ok = true;
					}
				}

				if (copyneeded) {
					try {
						// here we go when the user wants to *copy* the files
						if (ATT_COPY == msgOption) {
							// create and copy file...
							dest.createNewFile();
							// if we have a file which does not already exist, copy the source to the dest
							FileOperationsUtil.copyFile(f, dest, 1024);
						} // here we go when the user wants to *move* the files
						else if (ATT_MOVE == msgOption) {
							// if moving the file failed...
							if (!f.renameTo(dest)) {
								// ... show error msg
								JOptionPane.showMessageDialog(parentFrame, resourceMap.getString("errMsgFileMove"),
										resourceMap.getString("errMsgFileMoveTitle"), JOptionPane.PLAIN_MESSAGE);
							}
						}
						// add copied/moves file to link list...
						linkListModel.addElement((String) dest.getName());
						addedValues.add((String) dest.getName());
						// set the modified state
						modified = true;
						// set new default directory
						settingsObj.setLastOpenedAttachmentDir(f);
					} catch (IOException ex) {
						Constants.zknlogger.log(Level.SEVERE, ex.getLocalizedMessage());
						JOptionPane.showMessageDialog(parentFrame, resourceMap.getString("errMsgFileCopy"),
								resourceMap.getString("errMsgFileCopyTitle"), JOptionPane.PLAIN_MESSAGE);
					}
				}
			}
		} else if (ATT_CANCEL == msgOption) {
			// do nothing...
		} else if (ATT_REMAIN == msgOption) {
			// else add the text to the keyword-list (JList)
			for (File f : sources) {
				linkListModel.addElement((String) f.toString());
				addedValues.add((String) f.toString());
				// set new default directory
				settingsObj.setLastOpenedAttachmentDir(f);
			}
			// set the modified state
			modified = true;
		}
		// convert list to string array
		addedAttachments = addedValues.toArray(new String[addedValues.size()]);
		return modified;
	}

	/**
	 * This methods returns the path to an entry's attachment-file, which is passed
	 * as paraneter {@code linktye}. It creates an absolute path out from a relative
	 * path if necessray, which e.g. occures when having entry-attachments.
	 *
	 * @param settingsObj a reference to the CSettings-class
	 * @param dataObj
	 * @param linktype    a hyperlink-file, which is received when the user clicks
	 *                    on a hyperlink or linked attachment in e jEditorPane
	 * @return the absolute filepath to the linked file
	 */
	public static File getLinkFile(Settings settingsObj, Daten dataObj, String linktype) {
		File linkfile = new File(linktype);
		if (!linkfile.exists()) {
			String sepchar = String.valueOf(File.separatorChar);
			if (linkfile.toString().startsWith(sepchar)) {
				sepchar = "";
			}
			linkfile = new File(settingsObj.getAttachmentPath(dataObj.getUserAttachmentPath(), false) + sepchar
					+ linkfile.toString());
			if (!linkfile.exists()) {
				try {
					linkfile = new File(linkfile.getCanonicalPath());
					if (!linkfile.exists()) {
						linkfile = new File(linktype);
						File zknp = settingsObj.getMainDataFileDir();
						if (null == zknp) {
							zknp = new File(new File("").getAbsolutePath());
						}
						if (linkfile.toString().startsWith(sepchar)) {
							sepchar = "";
						}
						linkfile = new File(zknp.toString() + sepchar + linkfile.toString());
						if (!linkfile.exists()) {
							linkfile = new File(linkfile.getCanonicalPath());
						}
					}
				} catch (IOException | SecurityException ex) {
				}
			}
		}
		return linkfile;
	}

	/**
	 * This method checks whether the file's {@code imgfile} extenstion is
	 * corresponding to an image file, i.e. whether the file extension equals one of
	 * the most common image formats.
	 *
	 * @param imgfile the file which extension should be checked
	 * @return {@code true} if the the file seems to be an image, {@code false}
	 *         otherwise.
	 */
	public static boolean isImageFile(File imgfile) {
		boolean retval;
		String ext = FileOperationsUtil.getFileExtension(imgfile);
		retval = ext.equalsIgnoreCase("jpg") | ext.equalsIgnoreCase("png") | ext.equalsIgnoreCase("bmp")
				| ext.equalsIgnoreCase("tif") | ext.equalsIgnoreCase("tiff") | ext.equalsIgnoreCase("gif")
				| ext.equalsIgnoreCase("jpeg") | ext.equalsIgnoreCase("tga");
		return retval;
	}

	/**
	 * This method retrieves a complete formtag, extracts the information and
	 * converts the tag into the related image-path, which can be used for including
	 * the related form-image (which is stored as image-file in the subdirectory
	 * {@code Constants.FORMIMAGEPATH_SUBDIR}.
	 *
	 * @param formtag          the form-tag, which contains the form-data in
	 *                         UBB-format
	 * @param addFileExtension
	 * @param addLargeAppendix
	 * @return a string containing the file-name of the related form-image that
	 *         represents the graphical darstellung of the form-tag, or an empty
	 *         string if an error occured.
	 */
	public static String convertFormtagToImagepath(String formtag, boolean addFileExtension, boolean addLargeAppendix) {
		String imgpath = "";
		if (formtag != null && !formtag.isEmpty()) {
			formtag = formtag.replace(" ", "_");
			formtag = formtag.replace("=", "_");
			formtag = formtag.replace("#", "r");
			formtag = formtag.replace("|", "_");
			formtag = formtag.replace("^", "__");
			formtag = formtag.replace("[", "").replace("]", "");
			formtag = formtag.replace("\u00e4", "ae").replace("\u00c4", "Ae").replace("\u00f6", "oe")
					.replace("\u00d6", "Oe").replace("\u00fc", "ue").replace("\u00dc", "Ue").replace("\u00df", "ss")
					.replace("?", "_").replace("\"", "");
			imgpath = formtag;
			if (addLargeAppendix) {
				imgpath = imgpath + Constants.FORMIMAGE_LARGE_APPENDIX;
			}
			if (addFileExtension) {
				imgpath = imgpath + Constants.FORMIMAGE_EXTENSION;
			}
		}
		return imgpath;
	}

	/**
	 * This method checks whether the file's {@code imgfile} extenstion is
	 * corresponding to an image file and whether this image format is supported by
	 * the JEditorPane component.
	 *
	 * @param imgfile the file which extension should be checked
	 * @return {@code true} if the the file seems to be an image which is also
	 *         supported to be displayed in a JEditorPane, {@code false} otherwise.
	 */
	public static boolean isSupportedImageFile(File imgfile) {
		// TODO andere Grafikformate später, wenn unterstützt
		boolean retval;
		String ext = FileOperationsUtil.getFileExtension(imgfile);
		retval = ext.equalsIgnoreCase("jpg") | ext.equalsIgnoreCase("png") | ext.equalsIgnoreCase("gif")
				| ext.equalsIgnoreCase("jpeg");
		return retval;
	}

	/**
	 * This method checks whether a given string, usually passed as parameter from
	 * the {@link #eventHyperlinkActivated(javax.swing.event.HyperlinkEvent)
	 * eventHyperlinkActivated}, is a hyperlink or not. this is decided from the
	 * prefix, i.e. whether the string starts with common things like "http:" etc.
	 *
	 * @param linktype the clicked "hyperlink" from the hyperlink-event, as string
	 * @return {@code true} if the string seems to be a hyperlink, false otherwise
	 */
	public static boolean isHyperlink(String linktype) {
		return linktype.toLowerCase().startsWith("http://") || linktype.toLowerCase().startsWith("https://")
				|| linktype.toLowerCase().startsWith("ftp://") || linktype.toLowerCase().startsWith("webdav://")
				|| linktype.toLowerCase().startsWith("news:") || linktype.toLowerCase().startsWith("outlook:");
	}

	public static String getZettelkastenDataDir(Settings settings) {
		return getZettelkastenDataDir(settings, true);
	}

	/**
	 *
	 * @param settings
	 * @param addTrailingSeparatorChar
	 * @return
	 */
	public static String getZettelkastenDataDir(Settings settings, boolean addTrailingSeparatorChar) {
		// retrieve path of data file
		File f = settings.getMainDataFileDir();
		// check for valid value
		if (f != null) {
			// convert file path to string
			String path = f.getPath();
			// check whether trailing separator should be included or not
			if (addTrailingSeparatorChar) {
				path = path.concat(String.valueOf(File.separatorChar));
			}
			// retrieve substring, excluding filename
			return path;
		}
		return null;
	}

	/**
	 *
	 * @return
	 */
	public static String getWorkingDir() {
		return System.getProperty("user.dir").concat(String.valueOf(File.separatorChar));
	}

	/**
	 * Retrieve the {@code .Zettelkasten} subdirectory of the user home directory.
	 *
	 * @return The {@code .Zettelkasten} directory, which is a subdirectory of the
	 *         user.home directory, including a trailing separator char
	 *         ({@code <user-home>/.Zettelkasten/}).
	 */
	public static String getZettelkastenHomeDir() {
		return getZettelkastenHomeDir(true);
	}

	/**
	 * Retrieve the {@code .Zettelkasten} subdirectory of the user home directory.
	 *
	 * @param addTrailingSeparatorChar if {@code true}, a trailing separator char is
	 *                                 added.
	 * @return The {@code .Zettelkasten} directory, which is a subdirectory of the
	 *         user.home directory.
	 */
	public static String getZettelkastenHomeDir(boolean addTrailingSeparatorChar) {
		String fp = System.getProperty("user.home") + File.separatorChar + ".Zettelkasten";
		if (addTrailingSeparatorChar) {
			fp = fp + File.separatorChar;
		}
		return fp;
	}

	/**
	 * This method removes invalid characters from a file path.
	 * 
	 * @param path the (uncleaned) file path, which might contain illegal
	 *             characters.
	 * @return a cleaned path as {@code String} value, with illegal characters
	 *         removed.
	 */
	public static String getCleanFilePath(String path) {
		return path.replaceAll("[^a-zA-ZäöüÄÖÜß0-9.-]", "_");
	}
}
